# -*- coding: utf-8 -*-
#--------------------------------------------------------------------------
# Copyright (c) 2009 Barry Schwartz
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

import copy
import glob
import imp
import ocaml
import optparse
import os
import re
import shutil
import sys
import tempfile
import time
import traceback

from . import (config, kpathsea, pdf, pdfobj, pdf_crypto, temp_dir)

def kt_main(argv):

    exit_code = 0
    temporary_dir = None

    cipher_choices = ['AES:128'] + ['ARC4:' + str(n) for n in [128, 40]]
    cipher_help = cipher_choices[0]
    for s in cipher_choices[1:]:
        cipher_help = cipher_help + ', ' + s
    cipher_help = cipher_help + ' (default: %default)'

    try:
        parser = optparse.OptionParser(prog = argv[0],
                                       usage = "usage: %prog [options] document",
                                       formatter = optparse.TitledHelpFormatter())
        parser.add_option('-z', '--compression',
                          type = 'int', dest = 'compression', default = 6,
                          help = '0: no compression, 1..9 (default: %default)',
                          metavar = 'LEVEL')
        parser.add_option('-p', '--permissions',
                          dest = 'permissions', default = '',
                          help = ('p: printing, ' +
                                  'P: printing (degraded), ' +
                                  'm: modifying, ' +
                                  'f: filling forms, ' +
                                  'F: filling forms (restricted), ' +
                                  'c: copying, ' +
                                  'a: accessibility, ' +
                                  'A: assembly'),
                          metavar = 'FLAGS')
        parser.add_option('-o', '--owner-passphrase',
                          dest = 'owner_passphrase', default = '',
                          metavar = 'PASSPHRASE')
        parser.add_option('-u', '--user-passphrase',
                          dest = 'user_passphrase', default = '',
                          metavar = 'PASSPHRASE')
        parser.add_option('-c', '--cipher',
                          type = 'choice',
                          choices = cipher_choices,
                          dest = 'encryption_cipher', default = 'AES:128',
                          help = cipher_help,
                          metavar = 'CIPHER')
        parser.add_option('--no-hints', '--no-hinting',
                          action = 'store_true',
                          dest = 'no_hinting', default = False,
                          help = 'leave hinting out of embedded fonts')
        parser.add_option('--no-hex',
                          action = 'store_false',
                          dest = 'prefer_hex', default = True,
                          help = 'prefer (string) format over <hex> format')
        parser.add_option('-d', '--debug',
                          dest = 'debug_flags', default = '',
                          help = ('a: al commands, ' +
                                  'b: al bytecode, ' +
                                  'e: engine, ' +
                                  'i: input, ' +
                                  'g: galley breaking, ' +
                                  'l: line breaking, ' +
                                  'm: macro expansion, ' +
                                  'p: page breaking, ' +
                                  's: various stacks'),
                          metavar = 'FLAGS')

        (options, args) = parser.parse_args(args = argv[1:])
        if len(args) == 0:
            parser.error('no documents specified')
        if 2 <= len(args):
            parser.error('only one document can be specified')

        match = re.match(r'(.*):(.*)', options.encryption_cipher)
        encryption_cipher = match.group(1)
        key_length = int(match.group(2))

        ocaml.set_debug_flags(options.debug_flags)

        load_hyphenation_plugins(config.plugin_dir)

        source_document = args[0]
        base_name = os.path.basename(source_document)

        job = {
            'time'        : time.localtime(),
            'argv'        : argv,
            'jobname'     : base_name,
            'input_file'  : source_document,
            'output_file' : base_name + '.pdf',
            'log_file'    : base_name + '.log',
            }

        pdf.compression_params.level = options.compression
        pdf.font_params.no_hinting = options.no_hinting

        if options.prefer_hex:
            pdfobj.pdfobj_params.string_wrapper = pdfobj.pdf_hex_string
        else:
            pdfobj.pdfobj_params.string_wrapper = pdfobj.pdf_string

        if options.permissions != '':
            pdf_crypto.crypto_params.permissions = pdf.decode_permissions(options.permissions)
            pdf_crypto.crypto_params.key_length = key_length
            if options.encryption_cipher == 'AES':
                pdf_crypto.crypto_params.cipher = 'AES'
                pdf_crypto.crypto_params.encryption_algorithm = 4
                pdf_crypto.crypto_params.security_handler = 4
                pdf_crypto.crypto_params.metadata_encrypted = True
            else:
                pdf_crypto.crypto_params.cipher = 'ARC4'
                pdf_crypto.crypto_params.encryption_algorithm = 2
                pdf_crypto.crypto_params.security_handler = 3

            if options.owner_passphrase == '':
                pdf_crypto.crypto_params.owner_passphrase = None
            else:
                pdf_crypto.crypto_params.owner_passphrase = options.owner_passphrase.encode('ASCII')
            pdf_crypto.crypto_params.user_passphrase = options.user_passphrase.encode('ASCII')

        temporary_dir = temp_dir.temp_dir()
        initialize_kpathsea(argv[0])
        ocaml.log_open(job['log_file'])
        ocaml.initialize_run()

        ps = ocaml.create_parse_state(job)
        ocaml.istream_include_file(ocaml.current_istream(ps), job['input_file'])
        ocaml.initialize_parse_state(ps)
        ocaml.call_parse_state_hooks(ps)
        ast = ocaml.run_parser(ps, 'preamble')

        env = ocaml.initialize_environment ()
        builder = ocaml.void_builder()
        for n in ast:
            env = ocaml.eval_node(env, builder, n)
        pages = ocaml.get_pages(env)

        if False:
            import trace
            tracer = trace.Trace(count = 1,
                                 trace = 1,
                                 countfuncs = 0,
                                 countcallers = 0,
                                 ignoremods = (),
                                 ignoredirs = (),
                                 infile = None,
                                 outfile = None,
                                 timing = True)
            tracer.runfunc(pdf.py_write_pdf, job, pages)
            trace_results = tracer.results()
            trace_results.write_results(show_missing = True,
                                        coverdir = "/tmp")
        elif False:
            import cProfile as profile
            import pstats
            profiler = profile.Profile()
            profiler.runcall(pdf.py_write_pdf, job, pages)
            profiler.dump_stats('/tmp/profile.stats')
            stats = pstats.Stats('/tmp/profile.stats')
            stats.strip_dirs().sort_stats('time').print_stats(20)

        else:
            pdf.py_write_pdf(job, pages)

        ocaml.call_at_exit(ps)

    except OptionError:
        exit_code = 1

    except:
        print('PYTHON EXCEPTION:')
        print(traceback.print_exc())
        exit_code = 1

    if temporary_dir != None:
        shutil.rmtree(temporary_dir)

    return exit_code

#--------------------------------------------------------------------------

def load_hyphenation_plugins(directory):
    pattern = os.path.join(config.plugin_dir, '*_hyphen_' + ocaml.arithmetic_module() + '.cmxs')
    for p in glob.glob(pattern):
        ocaml.load_plugin(p)

def initialize_kpathsea(argv0):
    argv0 = copy.copy(argv0)
    ocaml.kpse_set_program_name(argv0, 'kt')
    if config.selfautoloc != None:
        os.environ['SELFAUTOLOC'] = config.selfautoloc
    if config.selfautodir != None:
        os.environ['SELFAUTODIR'] = config.selfautodir
    if config.selfautoparent != None:
        os.environ['SELFAUTOPARENT'] = config.selfautoparent
#    ocaml.kpse_init_prog('ANT', 300, 'cx', 'cmr10')
    ocaml.kpse_set_program_enabled(kpathsea.kpse_pk_format, 1, 1)
    ocaml.kpse_set_program_enabled(kpathsea.kpse_tfm_format, 1, 1)
    ocaml.kpse_set_program_enabled(kpathsea.kpse_tex_format, 0, 1)

#--------------------------------------------------------------------------

scripting_module_name = 'docsrc'
sys.modules[scripting_module_name] = imp.new_module(scripting_module_name)
ocaml_stuff = {
    'current_mode' : ocaml.current_mode,
    'current_istream' : ocaml.current_istream,
    'execute_next_char' : ocaml.execute_next_char,
    'execute_stream' : ocaml.execute_stream,
    'run_parser' : ocaml.run_parser,
    'open_node_list' : ocaml.open_node_list,
    'close_node_list' : ocaml.close_node_list,
    'create_parse_state' : ocaml.create_parse_state,
    'initialize_parse_state' : ocaml.initialize_parse_state,
    'call_parse_state_hooks' : ocaml.call_parse_state_hooks,

    'istream_exchange' : ocaml.istream_exchange,
    'istream_include_file' : ocaml.istream_include_file,
    'istream_insert_string' : ocaml.istream_insert_string,
    'istream_of_file' : ocaml.istream_of_file,
    'istream_of_string' : ocaml.istream_of_string,
    'insert_string' : ocaml.insert_string,

    'pdf_code' : ocaml.pdf_code,
    'update_pdf_info' : ocaml.update_pdf_info,
    'update_pdf_catalog' : ocaml.update_pdf_catalog,
    'register_pdf_object' : ocaml.register_pdf_object,
}
sys.modules[scripting_module_name].__dict__.update(ocaml_stuff)

def kt_exec(ps, python_code):
    exit_code = 0
    try:
        glob = sys.modules[scripting_module_name].__dict__
        glob['ps'] = ps
        exec(python_code, glob)
    except Exception as exc:
        (name, line, column) = ocaml.parse_state_location(ps)
        print('PYTHON EXCEPTION in ' + name + ' at line ' + str(line) + ', column ' + str(column) + ':')
        print(traceback.print_exc())
        exit_code = 1
    return exit_code

#--------------------------------------------------------------------------
